#!/usr/bin/python
# -*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

"""
Console mode implementation.
See :mod:`pylot.interfaces` for API documentation.

"""

__date__ = u"7 oct. 2009"
__author__ = u"INRIA pyLot"
__all__ = [
           u'ProcessHandler',
           u'LockHandler',
           u'IOHandler',
           u'DEFAULT_PROCESS_HANDLER',
           u'DEFAULT_IO_HANDLER'
           ]

import string
import subprocess
import sys
import threading

from traits.api import implements

from _pylot.minimal.commands import *
from _pylot.minimal.datum import Datum
from _pylot.minimal.utils import (uprint, error, check_implements)
from _pylot.minimal.system import ENCODING
from _pylot.minimal.writers import (StdTerminalWriter, NullWriter)

from _pylot.minimal.default.impl_processhandler import CommonProcessHandler

from _pylot.minimal.interfaces.i_iohandler import IIOHandler
from _pylot.minimal.interfaces.i_processhandler import (IProcessHandler,
     ILockHandler, ILock)
from _pylot.minimal.interfaces.i_streamwriter import IStreamWriter

from _pylot.minimal.default.impl_iohandler import CommonIOHandler
from _pylot.minimal.default.impl_commonrun import CommonRun

class IOHandler (CommonIOHandler):

  def __init__ (self, stdout=None, stderr=None, status=None,
                display_level=u'standard'):
    u"""
    :param stdout: Standard output text view
    :type stdout: QTextBrowser
    """
    # define default writers
    if stdout is None :
      stdout = StdTerminalWriter(sys.stdout)
    else :
      check_implements(u'stdout', stdout, IStreamWriter)
    if stderr is None :
      stderr = StdTerminalWriter(sys.stderr)
    else :
      check_implements(u'stderr', stderr, IStreamWriter)
    if status is None :
      status = NullWriter()
    else :
      check_implements(u'status', status, IStreamWriter)

    super(IOHandler, self).__init__(stdout, stderr, status,
                                    display_level=display_level)

    self.resetToDefaultWriters()

  def _rawInput (self, text):
    rep = raw_input(text.encode(ENCODING, u'replace'))
    return rep.decode(ENCODING, u'replace')

  def _yesNoQuestion(self, text):
    rep = self.rawInput(text)
    return Datum(None, bool, rep).value

"""
  def _enumerateContainer(self, book, bullet=u'*'):
    self.write(u'[%s]  %s' % (bullet, book.name), u'warn')
    datas = book.keys()
    datas.sort()
    for j, datum_id in enumerate(datas) :
      datum = book[datum_id]
      if datum.alias :
        alias = datum.alias
      else :
        alias = datum.name
      if not datum.value :
        datum.value = datum.default_value
      uprint(u'  [%d]  %-20s : %s' % (j + 1, alias, datum.value))
    return datas

  def _enumerateContainer(self, collection):

    books = collection.keys()
    books.sort()

    datas_dict = {}
    print
    for i, book_id in enumerate(books) :
      book = collection[book_id]
      datas_dict[book_id] = self._enumerateContainer(book, string.letters[i])
    print
    return books, datas_dict

  def _editDatum (self, datum, text):
    self.write(u'*** %s' % datum.alias, u'warn')
    for i, val in enumerate(datum.value_list) :
      uprint(u'\t%d. %s' % (i + 1, val))
    choice = raw_input(u'[%d-%s] or "enter" to add new value : ' % (
        1, len(datum.value_list)))
    try :
      choice = int(choice)
    except ValueError :
      new_val = raw_input(u'New value [%s] : ' % datum.default_value)
      datum.value = new_val
      datum.default_value = new_val
    else :
      datum.value = datum.value_list[choice - 1]
      datum.default_value = datum.value

  def _editContainer(self, book, text):

    if not text :
      text = u'Edit book %s' % book.name

    choice = u'X'
    while choice != u'q' :
      self.write(text, u'title')
      datas = self._enumerateContainer(book)
      choice = raw_input(u'Value to edit (ex: "1" or "q" to save and quit) ? ')
      if choice :
        try :
          datum_num = int(choice.strip()) - 1
        except ValueError :
          continue

        datum = book[datas[datum_num]]
        self._editDatum(datum, text)

  def _editContainer(self, collection, text):
    l2num = {}
    for num, l in enumerate(string.letters) :
      l2num[l] = num

    if not text :
      text = u'Edit collection %s' % collection.name

    choice = u'X'
    while choice != u'q' :
      self.write(text, u'title')
      books, datas_dict = self._enumerateContainer(collection)
      choice = raw_input(u'Value to edit (ex: "a,1" or "q" to save and quit) ? ')
      if choice :
        try :
          b_id, d_id = choice.split(u',')
        except ValueError :
          continue
        try :
          book_num = l2num[b_id.strip()]
        except KeyError:
          continue
        try :
          datum_num = int(d_id.strip()) - 1
        except ValueError :
          continue

        book_id = books[book_num]
        datum_id = datas_dict[book_id][datum_num]

        uprint(book_id, datum_id)
        datum = collection[book_id][datum_id]
        self._editDatum(datum, text)
"""

class Lock (object):
  implements(ILock)
  def __init__ (self):
    super(Lock, self).__init__()
    self.lock = threading.Lock()

  def acquire (self):
    self.lock.acquire()

  def release (self):
    self.lock.release()

class LockHandler (object):
  implements(ILockHandler)

  def newLock (self):
    return Lock()

class PythonRun (CommonRun):
  u"""
  Python "run object" using subprocess.Popen object
  """
  def start(self, python_method, args=[], kargs={}):
    self._state = self.Running
#    try :
    python_method(*args, **kargs)
#    except :
    self._return_code = 1
#    else :
    self._return_code = 0
    self._state = self.Finished
    return self._return_code

  def _getState (self):
    return self._state

  def _getReturnCode (self):
    return self._return_code

class ShellRun (CommonRun):

  def _getState (self):
    if self.thread.returncode is None :
      return self.Running
    else :
      return self.Finished

  def _getReturnCode (self):
    return self.thread.returncode

  def _kill (self, signal):
    self.thread.kill()

  def _terminate(self):
    self.thread.terminate()


class ProcessHandler (CommonProcessHandler):
  u"""
  >>> from pyLot.core import command_group_sample, ProcessHandler
  >>> handler = ProcessHandler()
  >>> cg = command_group_sample()
  >>> cg = handler.run(cg) #doctest: +SKIP
  ... Start "Command Group 1" ...
  py1: wake up
  >>> cg = handler.runCmd(u'pwd') #doctest: +SKIP
  """

  def __init__(self, *args, **kargs) :
    super(ProcessHandler, self).__init__(*args, **kargs)

    self.locks = {}
    self.lock_start_run = threading.Lock()
    self.modifying_locks = threading.Lock()

  def _init (self):
    self.io = IOHandler()

  @classmethod
  def runTextCommand (cls, cmd):
    if cmd.style == u'title' :
      uprint(u'=' * len(cmd.text))
    uprint(cmd.text)
    if cmd.style == u'title' :
      uprint(u'=' * len(cmd.text))

  def _waitCG (self, current_thread):
    cg = current_thread
    if cg._wait_list :
      for cg_to_wait in cg._wait_list :
        if cg_to_wait.state == CommonRun.NotRunning :
          # block this thread
          lock = threading.Lock()
          self.modifying_locks.acquire()
          if cg_to_wait in self.locks :
            self.locks[cg_to_wait].append(lock)
          else :
            self.locks[cg_to_wait] = []
            self.locks[cg_to_wait].append(lock)
          self.modifying_locks.release()
          lock.acquire()
          lock.acquire() # freeze this thread until an other thread free it
        elif cg_to_wait.state in (CommonRun.Running, CommonRun.Paused) :
          # block this thread
          cg_to_wait.thread.join()
        elif cg_to_wait.state == CommonRun.Finished :
          continue
        else :
          raise NotImplementedError, error(cg_to_wait.state)
    else :
      # DO NOT BLOCK
      return True

  def _realRun (self, command_group):
    self._waitCG(command_group)
    # all required group finished (success or fail)

    return_code = 0
    for cg_to_wait in command_group._wait_list :
      if cg_to_wait.return_code != 0 :
        return_code = cg_to_wait.return_code
        break

    if return_code == 0 :
      # run it
      self._realRun2(command_group)
    else :
      # force this group to fail
      uprint(tr_(u'Don\'t start "%s" because required CommandGroup'\
        u' "%s" fails (rcode=%s)') % (
          command_group.alias, cg_to_wait.alias, cg_to_wait.return_code),
          file=sys.stderr)
      command_group.cmds[-1].STATE = CommonRun.Finished
      command_group.cmds[-1].RETURN_CODE = return_code

    # cg is finished, we can release all locks
    self.modifying_locks.acquire()
    if command_group in self.locks :
      length = len(self.locks[command_group])
      for i in range(length) :
        self.locks[command_group].pop().release()
    self.modifying_locks.release()

  def _realRun2 (self, command_group):
    cg = command_group
    writers = self.io.textout_writers + cg.textout_writers
    text, style, level = self.formatTextCmdGroupStarted(command_group)
    self.io.write(text, style, level, writers=writers)

    cg.exec_time.start()

    # Get stdout and stderr mode
    # STDOUT
    if command_group.stdout_mode is None :
      stdout_mode = self.stdout_mode
    else :
      stdout_mode = command_group.stdout_mode
    # STDERR
    if command_group.stderr_mode is None :
      stderr_mode = self.stderr_mode
    else :
      stderr_mode = command_group.stderr_mode


    if u'save' in stdout_mode :
      stdout = subprocess.PIPE
    else :
      stdout = None
    if u'save' in stderr_mode :
      stderr = subprocess.PIPE
    else :
      stderr = None

    for i, cmd in enumerate(cg.cmds) :
      #a, b = cg.progress
      #percent = float(a + 1) / b * 100
      #self.io.write('[' + str(percent) + '% ]', level='verbose')
      text, style, level = self.formatTextCmdStarted(cmd)
      self.io.write(text, style, level, writers=writers)

      cmd.exec_time.start()
      # ManualCommand
      if type(cmd) in (ManualCommand, SpecialCommand) :
        # Manual Command finish successfully
        cmd.STATE = CommonRun.Finished
        if cmd.RETURN_CODE is None :
          cmd.RETURN_CODE = 0

      # TextCommand, PythonCommand
      elif type(cmd) in (TextCommand, PythonCommand) :
        run_obj = PythonRun(None)
        cmd.thread = run_obj
        if type(cmd) is TextCommand :
          run_obj.start(self.runTextCommand, [cmd])
        else :
          run_obj.start(cmd.python_method, cmd.args, cmd.kargs)
#          cmd.python_method(*cmd.args, **cmd.kargs)
      # ShellCommand
      elif type(cmd) is ShellCommand :
        # stdout and stderr ared defined before command loop (in this method)
        cwd = cmd.working_directory
        if not cwd.isdir() :
          cwd.mkdir()
        popen_obj = subprocess.Popen(cmd.shellUString(), shell=True,
                                     stdout=stdout, stderr=stderr, cwd=cwd)
        cmd.thread = ShellRun(popen_obj)
        cmd.stdout, cmd.stderr = popen_obj.communicate()

        # If var and screen outputs, we must print variables
        if u'save' in stdout_mode and u'write' in stdout_mode :
          self.io.write(cmd.stdout,
            writers=self.io.stdout_writers + cg.stdout_writers)
        if u'save' in stderr_mode and u'write' in stderr_mode :
          self.io.write(cmd.stderr,
            writers=self.io.stderr_writers + cg.stderr_writers)
      else :
        self.io.write(u'%s not implemented' % cmd, writers=writers)
      cmd.exec_time.stop()

      text, style, level = self.formatTextCmdExecTime(cmd)
      self.io.write(text, style, level, writers=writers)

      if cmd.return_code != 0 :
        break

      cg._progress[0] += 1

    cg.exec_time.stop()
    text, style, level = self.formatTextCmdGroupFinished(command_group)
    self.io.write(text, style, level, writers=writers)

    text, style, level = self.formatTextCmdGroupExecTime(command_group)
    self.io.write(text, style, level, writers=writers)

    return cg.return_code

  def _run (self, command_group, wait):
    self.lock_start_run.acquire()

    if self.autoclean.lower() in (u'error', u'output', u'all') :
      subprocess.Popen(u'clear', shell=True).communicate()

    if command_group.state == CommonRun.Running :
      txt = tr_(u'I cannot start command group because group is Running')
      uprint(txt, file=sys.stderr)
    elif command_group.state == CommonRun.Finished :
      txt = tr_(u'I cannot start command group because group has been launched and is finished')
      uprint(txt, file=sys.stderr)
    else :
#      command_group.cmds[0].STATE = ICommonRun.Running
      thread = threading.Thread(target=self._realRun,
                                args=[command_group])
      thread.start()
      command_group.thread = thread

      if wait is True :
        thread.join() # Ok
#        self._realRun(command_group)
#        self.requireGroups(command_group) # Must be debugged !!!

    self.lock_start_run.release()
    return command_group

  def _requireGroups(self, *args):
    for command_group in args :
      if command_group not in self._wait_list :
        self._wait_list.append(command_group)

    self._waitCG(command_group)
    # all required group finished (success or fail)

    self.modifying_locks.acquire()
    return_code = 0
    for cg_to_wait in self._wait_list :
      if cg_to_wait.return_code != 0 :
        return_code = cg_to_wait.return_code
        break

    # empty list
    length = len(self._wait_list)
    for i in range(length) :
      self._wait_list.pop()

    if return_code == 0 :
      # continue execution
      pass
    else :
      # signal a "required process" fails !
      try :
        group_alias = u'"%s"' % cg_to_wait.alias
      except NameError :
        group_alias = u'"?"'
      txt = u'required CommandGroup %s fails with return code %s' % (group_alias, return_code)
      self.io.write(txt,
        writers=self.io.textout_writers + cg_to_wait.textout_writers)
    self.modifying_locks.release()

DEFAULT_PROCESS_HANDLER = ProcessHandler()
DEFAULT_IO_HANDLER = DEFAULT_PROCESS_HANDLER.io

if __name__ == u'__main__':

  import doctest
  doctest.testmod()


